Saving and loading seamless contexts


In [1]:
#Download basic example context
import urllib.request
url = "https://raw.githubusercontent.com/sjdv1982/seamless/master/examples/basic.seamless"
urllib.request.urlretrieve(url, filename = "basic.seamless")


Out[1]:
('basic.seamless', <http.client.HTTPMessage at 0x7f81101196a0>)

In [2]:
import seamless
from seamless import cell, pythoncell, reactor, transformer
ctx = seamless.fromfile("basic.seamless")
await ctx.computation()


basic.seamless LOADED
Out[2]:
[]

In [3]:
ctx.tofile("basic-copy.seamless", backup=False)

Registrars

In the basic example, the code for the fibonacci function is defined in-line within the transformer. For a larger project that uses fibonacci in multiple places, you should define it separately. The standard way is to put it in a module and import it:


In [4]:
fib_module = open("fib.py", "w")
fib_module.write("""
def fibonacci(n):
    def fib(n):
        if n <= 1:
            return [1]
        elif n == 2:
            return [1, 1]
        else:
            fib0 = fib(n-1)
            return fib0 + [ fib0[-1] + fib0[-2] ]
    fib0 = fib(n)
    return fib0[-1]
""")
fib_module.close()

ctx.formula.set("""
from fib import fibonacci   #  Bad!
return fibonacci(a) + fibonacci(b)
""")


Out[4]:
Seamless cell: .formula

But if we do this, we immediately lose live feedback. There is no way that seamless can guess that a change in fib.py should trigger a re-execution of ctx.formula's transformer. Even if you manually force a re-execution, with ctx.formula.touch(), this will not change anything: the fib module has already been imported by Python. Python's import mechanism is rather hostile to live code changes, and it is difficult to reload any kind of module. While possible to force manually (e.g. using %autoreload), it does not always work. Anyway, all this manual forcing is against the spirit of seamless.

With seamless, only use import for external libraries. Avoid importing any project code.

Instead of Python imports, seamless has a different mechanism: registrars.

First, let's link fib.py to a cell:


In [5]:
from seamless.lib import link, edit
ctx.fib  = pythoncell()
ctx.link_fib = link(ctx.fib, ".", "fib.py") #Loads the cell from the existing fib.py 
ctx.ed_fib = edit(ctx.fib, "Fib module")

Then, we will register the fib cell with the Python registrar, and connect the fibonacci Python function object from the Python registrar to the transformer.

This will re-establish live feedback: whenever fib.py gets changed, the transformer will execute with the new code.


In [6]:
rpy = ctx.registrar.python
rpy.register(ctx.fib)

rpy.connect("fibonacci", ctx.transform)
ctx.formula.set("return fibonacci(a) + fibonacci(b)")


Out[6]:
Seamless cell: .formula

For the next section, we will build a new context. You can destroy a context cleanly with context.destroy() (Just re-defining ctx should work also, but not inside the Jupyter Notebook)


In [7]:
ctx.destroy()

Array cells


In [8]:
import seamless
from seamless import cell, reactor, transformer
ctx = seamless.context()

ctx.x = cell("array")
ctx.y = cell("array")

In [9]:
import numpy as np
arr = np.linspace(0, 100, 200)
ctx.x.set(arr)
ctx.x.value[:10]


Out[9]:
array([ 0.        ,  0.50251256,  1.00502513,  1.50753769,  2.01005025,
        2.51256281,  3.01507538,  3.51758794,  4.0201005 ,  4.52261307])

In [10]:
arr2 = -0.5 * arr**2 + 32 * arr - 12
ctx.y.set(arr2)


Out[10]:
Seamless cell: .y

In [39]:
import bqplot
from bqplot import pyplot as plt
fig = plt.figure()
plt.plot(ctx.x.value, ctx.y.value)
plt.show()


Warning While cell.value, inputpins and editpins return numpy arrays, seamless assumes that you don't modify them in-place

Preliminary transformer results

Let's pretend that ctx.computation performs some complicated scientific computation:


In [12]:
t = ctx.computation = transformer({
    "amplitude": {"pin": "input", "dtype": "float"},
    "frequency": {"pin": "input", "dtype": "float"},
    "gravity": {"pin": "input", "dtype": "float"},
    "temperature": {"pin": "input", "dtype": "float"},
    "mutation_rate": {"pin": "input", "dtype": "float"},
     "x": {"pin": "input", "dtype": "array"},
     "y": {"pin": "output", "dtype": "array"},
})

In [13]:
ctx.amplitude = cell("float").set(4)
ctx.amplitude.connect(t.amplitude)
ctx.frequency = cell("float").set(21)
ctx.frequency.connect(t.frequency)
ctx.gravity = cell("float").set(9.8)
ctx.gravity.connect(t.gravity)
ctx.temperature = cell("float").set(298)
ctx.temperature.connect(t.temperature)
ctx.mutation_rate = cell("float").set(42)
ctx.mutation_rate.connect(t.mutation_rate)
ctx.x.connect(t.x)
t.y.connect(ctx.y)

In [14]:
ctx.computation.code.cell().set("""
import numpy as np
import time
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 20):
    pos = int(n/20*len(y))
    return_preliminary(y[:pos])
    time.sleep(1)
return y 
""")


Out[14]:
Seamless cell: .cell1

In [15]:
ctx.computation.code.cell().touch()

Run the cell above, then repeatedly run the cell below


In [40]:
v = len(ctx.y.value)
print(v)
plt.clear()
plt.plot(ctx.x.value[:v], ctx.y.value)
plt.xlim(0,100)
plt.show()


200

Simple macros

Now let's assume that in the example above, we forgot a parameter "radius". To implement it, we would have to re-declare the transformer with the extra input pin, re-declare the connections, and re-define the code cells. This is very annoying, and it is easy to make a mistake! However, transformer and reactor are macros, which means that they accept cells as input. So we can declare the computation parameters as a cell, and when we want to modify them, we just modify the cell.

Below is a re-factor:


In [17]:
ctx.computation_params = cell("json").set({
    "amplitude": {"pin": "input", "dtype": "float"},
    "frequency": {"pin": "input", "dtype": "float"},
    "gravity": {"pin": "input", "dtype": "float"},
    "temperature": {"pin": "input", "dtype": "float"},
    "mutation_rate": {"pin": "input", "dtype": "float"},
     "x": {"pin": "input", "dtype": "array"},
     "y": {"pin": "output", "dtype": "array"},
})

In [18]:
t = ctx.computation = transformer(ctx.computation_params)

and then the same as before...


In [19]:
ctx.amplitude = cell("float").set(4)
ctx.amplitude.connect(t.amplitude)
ctx.frequency = cell("float").set(21)
ctx.frequency.connect(t.frequency)
ctx.gravity = cell("float").set(9.8)
ctx.gravity.connect(t.gravity)
ctx.temperature = cell("float").set(298)
ctx.temperature.connect(t.temperature)
ctx.mutation_rate = cell("float").set(42)
ctx.mutation_rate.connect(t.mutation_rate)
ctx.x.connect(t.x)
t.y.connect(ctx.y)

In [20]:
ctx.computation.code.cell().set("""
import numpy as np
import time
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 20):
    pos = int(n/20*len(y))
    return_preliminary(y[:pos])
    time.sleep(1)
return y 
""")


Out[20]:
Seamless cell: .cell2

In [21]:
ctx.computation.code.cell().touch()

Again, to see the plot, run the cell above, then repeatedly run the cell below


In [41]:
v = len(ctx.y.value)
print(v)
plt.clear()
plt.plot(ctx.x.value[:v], ctx.y.value)
plt.xlim(0,100)
plt.show()


200

Now we can add a parameter, and seamless will re-connect everything.


In [23]:
d = ctx.computation_params.value
d["radius"] = {"pin": "input", "dtype": "float"}
ctx.computation_params.set(d)


Macro object re-computation Seamless cell: .computation_params 0 .computation
DONE DESTROY
CONNECTION: mode 'input', source Seamless cell: .gravity, dest ('gravity',)
CONNECTION: mode 'input', source Seamless cell: .amplitude, dest ('amplitude',)
CONNECTION: mode 'input', source Seamless cell: .frequency, dest ('frequency',)
CONNECTION: mode 'input', source Seamless cell: .mutation_rate, dest ('mutation_rate',)
CONNECTION: mode 'input', source Seamless cell: .x, dest ('x',)
CONNECTION: mode 'input', source Seamless cell: .cell2, dest ('code',)
CONNECTION: mode 'input', source Seamless cell: .temperature, dest ('temperature',)
CONNECTION: mode 'output', source ('y',), dest Seamless cell: .y
Out[23]:
Seamless cell: .computation_params

In [24]:
ctx.radius = cell("float").set(10)
ctx.radius.connect(ctx.computation.radius)

This will restart the computation. If you like, you can now modify the code of ctx.computation.code.cell() to take into account the value of radius.

ctx.computation_params is a JSON cell. It can be linked to the hard disk and then edited to the hard disk like any other cell:


In [25]:
from seamless.lib import link
ctx.link1 = link(ctx.computation_params, ".", "computation_params.json")

Now, whenever you modify "computation_params.json", the transformer macro will be re-executed.

However, JSON is very unforgiving when it comes to commas and braces. Therefore, it is recommended that you declare ctx.computation_params as cell("cson") instead. In seamless, JSON and CSON have a special relationship: you can provide a CSON cell whenever a JSON cell is expected, and seamless will make the conversion implicitly.

Creating your own macros

Seamless macros can be declared with the @macro decorator. The following macro does the same as above:


In [26]:
from seamless import macro

@macro("json")
def create_computation(ctx, params):
    from seamless import transformer, cell, pythoncell
    ctx.computation = transformer(params)
    ctx.computation_code = pythoncell().set("""
import numpy as np
import time
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 20):
    pos = int(n/20*len(y))
    return_preliminary(y[:pos])
    time.sleep(1)
return y 
""")
    ctx.computation_code.connect(ctx.computation.code)
    ctx.export(ctx.computation) #creates a pin on ctx for every unconnected pin on ctx.computation
    
ctx.computation = create_computation(ctx.computation_params)

Let's add a little convenience function to reconnect the computation pins:


In [27]:
def connect_computation(t):
    ctx.amplitude = cell("float").set(4)
    ctx.amplitude.connect(t.amplitude)
    ctx.frequency = cell("float").set(21)
    ctx.frequency.connect(t.frequency)
    ctx.gravity = cell("float").set(9.8)
    ctx.gravity.connect(t.gravity)
    ctx.temperature = cell("float").set(298)
    ctx.temperature.connect(t.temperature)
    ctx.mutation_rate = cell("float").set(42)
    ctx.mutation_rate.connect(t.mutation_rate)
    ctx.radius = cell("float").set(10)
    ctx.radius.connect(ctx.computation.radius)    
    ctx.x.connect(t.x)
    t.y.connect(ctx.y)
    
connect_computation(ctx.computation)

... and plot the results


In [42]:
v = len(ctx.y.value)
print(v)
plt.clear()
plt.plot(ctx.x.value[:v], ctx.y.value)
plt.xlim(0,100)
plt.show()


200

The source code of the macro is added to the context, and it will be saved when the context is saved. Whenever ctx.computation_params changes, it will be re-executed. In the next version of seamless, you will be able to edit the macro source code inside a cell. But for now, we have to just re-define it.

Let's assume that our scientific computation consists of two parts: a slow computation that depends only on amplitude and frequency, and a fast analysis of the result that depends on everything else. Using a macro, we can split the computation, and optionally omit the analysis.


In [29]:
@macro({"params": "json", "run_analysis": "bool"})
def create_computation(ctx, params, run_analysis):
    from seamless import transformer, cell, pythoncell
    from seamless.core.worker import ExportedOutputPin
    
    # Slow computation
    params_computation = {
        "amplitude": {"pin": "input", "dtype": "float"},
        "frequency": {"pin": "input", "dtype": "float"},
        "x": {"pin": "input", "dtype": "array"}, 
        "y": {"pin": "output", "dtype": "array"},  
    }
    ctx.computation = transformer(params_computation)
    ctx.computation_code = pythoncell().set("""
import numpy as np
import time
print("start slow computation")
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 5):
    pos = int(n/5*len(y))
    return_preliminary(y[:pos])
    time.sleep(1)
return y 
""")
    ctx.computation_code.connect(ctx.computation.code) 
    ctx.computation_result = cell("array")     
    ctx.computation.y.connect(ctx.computation_result)
    
    # Fast analysis
    params2 = params.copy()
    for k in params_computation:
        if k not in ("x", "y"):
            params2.pop(k, None)
    ctx.analysis = transformer(params2)
    ctx.analysis_code = pythoncell().set("print('start analysis'); return x")
    ctx.analysis_code.connect(ctx.analysis.code)         
    
    # Final result
    ctx.result = cell("array")    
    if run_analysis:        
        ctx.computation_result.connect(ctx.analysis.x)
        ctx.analysis.y.connect(ctx.result)
    else:
        ctx.computation_result.connect(ctx.result)
    ctx.y = ExportedOutputPin(ctx.result) 
    ctx.export(ctx.computation, skipped=["y"])
    ctx.export(ctx.analysis, skipped=["x","y"])

ctx.run_analysis = cell("bool").set(True)        
ctx.computation = create_computation(
    params=ctx.computation_params, 
    run_analysis=ctx.run_analysis
)

connect_computation(ctx.computation)

As you see, the slow computation starts immediately. Every second, for five seconds, the computation returns the results so far. The results are forwarded to the analysis (which, in this dummy example, does nothing).

Now, if we change the radius parameter (or gravity, or temperature, or mutation_rate), the analysis will be re-executed, but not the slow computation


In [30]:
ctx.radius.set(2)


start slow computation
Out[30]:
Seamless cell: .radius

On the other hand, changing amplitude or frequency re-launches the entire computation


In [31]:
ctx.amplitude.set(21)


start analysis
Out[31]:
Seamless cell: .amplitude

We can toggle run_analysis on and off, and the macro will re-build the computation context


In [32]:
ctx.run_analysis.set(False)


start slow computation
Macro object re-computation Seamless cell: .run_analysis run_analysis .computation
DONE DESTROY
CONNECTION: mode 'input', source Seamless cell: .gravity, dest ('analysis', 'gravity')
CONNECTION: mode 'input', source Seamless cell: .mutation_rate, dest ('analysis', 'mutation_rate')
CONNECTION: mode 'input', source Seamless cell: .radius, dest ('analysis', 'radius')
CONNECTION: mode 'input', source Seamless cell: .temperature, dest ('analysis', 'temperature')
CONNECTION: mode 'input', source Seamless cell: .frequency, dest ('computation', 'frequency')
CONNECTION: mode 'input', source Seamless cell: .x, dest ('computation', 'x')
CONNECTION: mode 'input', source Seamless cell: .amplitude, dest ('computation', 'amplitude')
CONNECTION: mode 'alias', source ('result',), dest Seamless cell: .y
Out[32]:
Seamless cell: .run_analysis

In [33]:
ctx.run_analysis.set(True)


start slow computation
Macro object re-computation Seamless cell: .run_analysis run_analysis .computation
DONE DESTROY
CONNECTION: mode 'input', source Seamless cell: .gravity, dest ('analysis', 'gravity')
CONNECTION: mode 'input', source Seamless cell: .mutation_rate, dest ('analysis', 'mutation_rate')
CONNECTION: mode 'input', source Seamless cell: .radius, dest ('analysis', 'radius')
CONNECTION: mode 'input', source Seamless cell: .temperature, dest ('analysis', 'temperature')
CONNECTION: mode 'input', source Seamless cell: .frequency, dest ('computation', 'frequency')
CONNECTION: mode 'input', source Seamless cell: .x, dest ('computation', 'x')
CONNECTION: mode 'input', source Seamless cell: .amplitude, dest ('computation', 'amplitude')
CONNECTION: mode 'alias', source ('result',), dest Seamless cell: .y
Out[33]:
Seamless cell: .run_analysis

Unfortunately, the re-building of the computation context also re-launches the slow computation.

However, seamless has (experimental!) caching for macros, which does not re-execute transformers whose inputs have not changed. It can be enabled with the with_caching parameter


In [34]:
@macro({"params": "json", "run_analysis": "bool"}, with_caching = True)
def create_computation(ctx, params, run_analysis):
    # For the rest of the cell, as before ....
    # ...
    # ...
    
    from seamless import transformer, cell, pythoncell
    from seamless.core.worker import ExportedOutputPin
    
    # Slow computation
    params_computation = {
        "amplitude": {"pin": "input", "dtype": "float"},
        "frequency": {"pin": "input", "dtype": "float"},
        "x": {"pin": "input", "dtype": "array"}, 
        "y": {"pin": "output", "dtype": "array"},  
    }
    ctx.computation = transformer(params_computation)
    ctx.computation_code = pythoncell().set("""
import numpy as np
import time
print("start slow computation")
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 5):
    pos = int(n/5*len(y))
    return_preliminary(y[:pos])
    time.sleep(1)
return y 
""")
    ctx.computation_code.connect(ctx.computation.code) 
    ctx.computation_result = cell("array")     
    ctx.computation.y.connect(ctx.computation_result)
    
    # Fast analysis
    params2 = params.copy()
    for k in params_computation:
        if k not in ("x", "y"):
            params2.pop(k, None)
    ctx.analysis = transformer(params2)
    ctx.analysis_code = pythoncell().set("print('start analysis'); return x")
    ctx.analysis_code.connect(ctx.analysis.code)         
    
    # Final result
    ctx.result = cell("array")    
    if run_analysis:        
        ctx.computation_result.connect(ctx.analysis.x)
        ctx.analysis.y.connect(ctx.result)
    else:
        ctx.computation_result.connect(ctx.result)
    ctx.y = ExportedOutputPin(ctx.result) 
    ctx.export(ctx.computation, skipped=["y"])
    ctx.export(ctx.analysis, skipped=["x","y"])

ctx.run_analysis = cell("bool").set(True)        
ctx.computation = create_computation(
    params=ctx.computation_params, 
    run_analysis=ctx.run_analysis
)

connect_computation(ctx.computation)
await ctx.computation()


start slow computation
start slow computation
start analysis
Waiting for: ['.computation.computation']
start analysis
start analysis
start analysis
start analysis
Out[34]:
[]

Now, when we toggle run_analysis, it will no longer re-run the computation


In [35]:
ctx.run_analysis.set(False)
await ctx.computation()


Macro object re-computation Seamless cell: .run_analysis run_analysis .computation
DONE DESTROY
CONNECTION: mode 'input', source Seamless cell: .gravity, dest ('analysis', 'gravity')
CONNECTION: mode 'input', source Seamless cell: .mutation_rate, dest ('analysis', 'mutation_rate')
CONNECTION: mode 'input', source Seamless cell: .radius, dest ('analysis', 'radius')
CONNECTION: mode 'input', source Seamless cell: .temperature, dest ('analysis', 'temperature')
CONNECTION: mode 'input', source Seamless cell: .frequency, dest ('computation', 'frequency')
CONNECTION: mode 'input', source Seamless cell: .x, dest ('computation', 'x')
CONNECTION: mode 'input', source Seamless cell: .amplitude, dest ('computation', 'amplitude')
CONNECTION: mode 'alias', source ('result',), dest Seamless cell: .y
Waiting for: ['.computation.analysis']
Out[35]:
[]

In [36]:
ctx.run_analysis.set(True)
await ctx.computation()


Macro object re-computation Seamless cell: .run_analysis run_analysis .computation
DONE DESTROY
CONNECTION: mode 'input', source Seamless cell: .gravity, dest ('analysis', 'gravity')
CONNECTION: mode 'input', source Seamless cell: .mutation_rate, dest ('analysis', 'mutation_rate')
CONNECTION: mode 'input', source Seamless cell: .radius, dest ('analysis', 'radius')
CONNECTION: mode 'input', source Seamless cell: .temperature, dest ('analysis', 'temperature')
CONNECTION: mode 'input', source Seamless cell: .frequency, dest ('computation', 'frequency')
CONNECTION: mode 'input', source Seamless cell: .x, dest ('computation', 'x')
CONNECTION: mode 'input', source Seamless cell: .amplitude, dest ('computation', 'amplitude')
CONNECTION: mode 'alias', source ('result',), dest Seamless cell: .y
start analysis
Out[36]:
[]

Creating an interactive dashboard

Jupyter has its own widget library for IPython kernels, called ipywidgets. In turn, several other visualization libraries, e.g. bqplot, are built upon ipywidgets.

ipywidgets uses traitlets to perform data synchronization. Below is a code snippet that uses seamless.observer and traitlets.observe to link a seamless cell to a traitlet (it will be included in the next seamless release).


In [37]:
import traitlets
from collections import namedtuple
import traceback

def traitlink(c, t):
    assert isinstance(c, seamless.core.Cell)
    assert isinstance(t, tuple) and len(t) == 2
    assert isinstance(t[0], traitlets.HasTraits)
    assert t[0].has_trait(t[1])
    handler = lambda d: c.set(d["new"])
    value = c.value
    if value is not None:
        setattr(t[0], t[1], value)
    else:
        c.set(getattr(t[0], t[1]))
    def set_traitlet(value):
        try:
            setattr(t[0], t[1], value)
        except:
            traceback.print_exc()
    t[0].observe(handler, names=[t[1]])
    obs = seamless.observer(c, set_traitlet )
    result = namedtuple('Traitlink', ["unobserve"])
    def unobserve():
        nonlocal obs
        t[0].unobserve(handler)
        del obs  
    result.unobserve = unobserve
    return result

With this, we can create a nice little interactive dashboard for our scientific protocol:


In [38]:
# Clean up any old traitlinks, created by repeated execution of this cell
try:
    for t in traitlinks:
        t.unobserve()
except NameError:
    pass

from IPython.display import display

from ipywidgets import Checkbox, FloatSlider
w_amp = FloatSlider(description = "Amplitude")
w_freq = FloatSlider(description = "Frequency")
w_ana = Checkbox(description="Run analysis")

traitlinks = [] # You need to hang on to the object returned by traitlink
traitlinks.append( traitlink(ctx.amplitude, (w_amp, "value")) )

traitlinks.append( traitlink(ctx.frequency, (w_freq, "value")) )
traitlinks.append( traitlink(ctx.run_analysis, (w_ana, "value")) )

import bqplot
from bqplot import pyplot as plt
fig = plt.figure()
plt.plot(np.zeros(1), np.zeros(1))
plt.xlim(0,100)
plt.ylim(-100,100)

traitlinks.append( traitlink(ctx.x, (fig.marks[0], "x")) )
traitlinks.append( traitlink(ctx.y, (fig.marks[0], "y")) )

display(w_amp)
display(w_freq)
display(w_ana)
display(fig)

ctx.run_analysis.set(False)
await ctx.computation()


Macro object re-computation Seamless cell: .run_analysis run_analysis .computation
DONE DESTROY
CONNECTION: mode 'input', source Seamless cell: .gravity, dest ('analysis', 'gravity')
CONNECTION: mode 'input', source Seamless cell: .mutation_rate, dest ('analysis', 'mutation_rate')
CONNECTION: mode 'input', source Seamless cell: .radius, dest ('analysis', 'radius')
CONNECTION: mode 'input', source Seamless cell: .temperature, dest ('analysis', 'temperature')
CONNECTION: mode 'input', source Seamless cell: .frequency, dest ('computation', 'frequency')
CONNECTION: mode 'input', source Seamless cell: .x, dest ('computation', 'x')
CONNECTION: mode 'input', source Seamless cell: .amplitude, dest ('computation', 'amplitude')
CONNECTION: mode 'alias', source ('result',), dest Seamless cell: .y
Waiting for: ['.computation.analysis']
Out[38]:
[]

In [ ]: